MacでNo GUI, No Dockなコマンドラインアプリケーションを作る


概要

コマンドラインアプリって何。

→コマンドラインから起動できるアプリです。


とあるアプリからとあるアプリへとMacのFocusを高速で移すアプリを作ったので、備忘録。


具体的な用途として、

Unityに他アプリで編集したコードのビルドをしてもらうためには、

UnityにFocusを移すのが一番、ということで、そういうMacのアプリケーションを作る話



成果

出来た物がこちらになります。

https://github.com/sassembla/SwitchApp



前提

次の条件を満たしたい。


1.一瞬だけFocusをUnityに移して次の瞬間別の任意のアプリにFocusを移す

→これでUnityビルドが走る。


2.コマンドラインから起動する

→最終的に完成した.app内のexec自体を、コマンドラインから起動する事を予定する。


3.コマンドラインから値を受け取りたい

→ある程度値をparameterとして渡せるようにしたい。

4.GUIを持たず、Dockにも表示されず、そのアプリケーション自体は一瞬もFocusを得ないようにしたい

→設定でなんとかなるのか?


5.確実に、100% "コマンドが順に行われる事" を保証したい

→以前コマンドラインから、open -a "アプリ名" などと行った場合、挙動の終了を待てず、挙動が安定しなかった。

そのへん改善したい。



1.一瞬だけFocusをUnityに移して次の瞬間別の任意のアプリにFocusを移す

まずFocusをあてるアプリをfromApp、次にFocusを当てるアプリをtoAppとすると、

このアプリ起動 > fromAppにFocus > toAppにFocus > このアプリ終了

とかすればいいので、はい。


Unityのアプリケーションの特性として、Focusがあたると既存のアプリのビルドを始めてくれるので、

それを狙って引き起こす。


MacのFocusを弄るAPIが複数見つかったので、試してみる。


ちなみに、コマンドラインツール っていうXcodeのテンプレートから作れる種類のアプリがあるんだけど、

それだと、純粋にCな感じで、Obj-Cの世界とかMacの世界へとアクセスするコードから書かねばならず、

とても辛かったので、MacのAppから実行する形を取っている。



2.コマンドラインから起動する

最終的にコンパイルした.appについて、

コンテキストメニュー >  パッケージの内容を表示 > Contents > MacOS > "アプリ名" っていうUnix実行ファイルが入っているので、

コレを使う。

スクリーンショット 2013-04-12 14.14.03.png

これを、コマンドラインから実行するのは、

exec ./SwitchApp $@


とかすれば、引数含めて渡して実行する事が出来る。



3.コマンドラインから値を受け取りたい

正直死にたい実装になった。もっとあるだろ。


NSApplication をextendsしたclass SwitchApp を定義し、main.mから実行される

int NSApplicationMain(int argc, const char *argv[]); メソッドをオーバーライド、


SwitchApp.h

@interface SwitchApp : NSApplication

int NSApplicationMain(int argc, const char *argv[]);

@end

①.m側で、appDelegateを自分で初期化し、Delegateにセット

        AppDelegate * delegate = [[AppDelegate alloc] init];


②argc と argv から、値を抽出し、

key-valueの形にして、appDelegateへと渡す。

        [delegate setArgs:argsDict];


③delegate側の- (void)applicationDidFinishLaunching:(NSNotification * ) aNotification メソッドを直で呼ぶ方法がなく、

アプリケーション自体の起動には

        [NSApp run];

を使うしか無い(遅延とかはできるんだけどNotificationの内容に干渉する手段はない?)っぽい。


②で値をグローバルな値として事前に渡し、[NSApp run]までの間、綱渡りしてるのが悔恨。

何か方法あると思う。


→runした後にメソッド実行して渡すっていうかそこから実行とかすればよかったですね!!!


4.GUIを持たず、Dockにも表示されず、そのアプリケーション自体は一瞬もフォーカスを得ないようにしたい

GUIナシ、Dockにも映らない設定のMacアプリを作るには、


Application is background only : UIを持たない

Application is agent (UIElement) : Dockや強制終了リストに映らない

をプロパティに宣言してビルドすればOK。


スクリーンショット 2013-04-12 13.50.43.png


詳細は下記

http://developer.apple.com/library/ios/#documentation/general/Reference/InfoPlistKeyReference/Articles/LaunchServicesKeys.html



5.確実に、100% "コマンドが順に行われる事" を保証したい

現在動いているアプリ一覧からApplicationのBundleIdentifierを取得し、activateさせる、みたいな方法があったのだが、

これが結局、ほぼ非同期でアクションを行っているようで、

2連続させると正常に動作しないことがあった。


一応下記のコードになる。 fromAppを文字列で定義してある。

NSString * fromApp = @"アプリ名";

NSArray * apps = [[NSWorkspace sharedWorkspace] runningApplications];

for (NSRunningApplication * app in apps) {

if([app.bundleIdentifier.lowercaseString hasPrefix:fromApp]) {

[app activateWithOptions:NSApplicationActivateIgnoringOtherApps];

}

}


for文の外まで各アプリを引っ張って、並べて実行しても、行単位でブロックするわけでは無いらしく、Focusの移る順が前後することがあったため、

10回に1回くらい、from to 2つのアプリのうち、toが全面に来ない、という事があった。


そこで、ScriptingBridgeを使う実装に変更。

id fromApp = [SBApplication applicationWithBundleIdentifier:fromAppStr];

if (fromApp) {

[fromApp activate];

} 

これで、Scriptの実行が完了するのが保証され、正しくfrom to の順にFocusが移り、toアプリが前面に残る結果になった。

かなり高速に動作するようなのだけれど、どうしても一瞬ちらつく。ぐぬぬ。まいいか。



以上。